package api

import (
	"io"
	"mime/multipart"
	"moss/conf"
	"moss/fs"
	"moss/meta"
	"moss/proto"
	"moss/status"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

func PutObject(requestId string, accessKeyId string, bucketName string, objectName string, document io.ReadCloser,
	contentLength int64, contentChecksum, encryption, permission, userMeta string) (string, status.Status) {

	if contentLength <= 0 {
		return "", status.MissingContentLength
	}

	if contentLength > conf.Config.Limit.MaxFileSize {
		return "", status.InvalidArgument
	}

	if !validIdentifier(bucketName) {
		return "", status.InvalidBucketName
	}

	if !validIdentifier(objectName) {
		return "", status.InvalidObjectName
	}

	if len(encryption) != 0 && encryption != "AES256" {
		return "", status.InvalidEncryptionAlgorithmError
	}

	if getSizeOfUserMeta(userMeta) > conf.UserMetaMaxSize {
		return "", status.InternalError
	}

	objectMeta, ok := meta.GetObjectInfo(bucketName, objectName)
	if ok {
		if !checkObjectAclByBucketName(objectMeta.BucketName, objectMeta.Permission, objectMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
			return "", status.AccessDenied
		}
	} else {
		bucketMeta, ok := meta.GetBucketInfo(bucketName)
		if !ok {
			return "", status.NoSuchBucket
		}
		if !checkBucketACL(bucketMeta.Permission, bucketMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
			return "", status.AccessDenied
		}
	}

	fileLocation := fs.GetObjectLocation(bucketName, objectName, 0)
	rc := fs.WriteFile(fileLocation, document, contentLength, encryption)
	if rc != status.Success {
		return "", rc
	}

	tagChan := fs.CalculateMD5Async(fileLocation, encryption)
	crc64Chan := fs.CalculateCRC64Async(fileLocation)

	tag := <-tagChan
	crcChecksum := <-crc64Chan
	if len(tag) == 0 || len(crcChecksum) == 0 {
		return "", status.InternalError
	}

	if len(contentChecksum) > 0 && contentChecksum != fs.MD5Hex2Base64(tag) {
		fs.RemoveFile(fileLocation)
		return "", status.InvalidDigest
	}

	meta.SetObjectInfo(bucketName, objectName, accessKeyId, tag, fileLocation, permission, encryption,
		conf.ObjectTypeNormal, "", userMeta, crcChecksum, time.Now().Format(conf.OssTimeFormat), contentLength)

	return tag, status.Success
}

func CopyObject(requestId, accessKeyId, srcBucketName, srcObjectName, dstBucketName, dstObjectName, srcIfModifiedSince, srcIfUnmodifiedSince,
	srcIfMatch, srcIfNoneMatch, metaDirective, encryption, objectACL, userMeta string) (*proto.CopyObjectResult, status.Status) {

	metaDirective = strings.ToUpper(metaDirective)
	if metaDirective != conf.OssMetadataDirectiveCopy && metaDirective != conf.OssMetadataDirectiveReplace {
		return nil, status.InvalidArgument
	}

	if len(encryption) != 0 && encryption != "AES256" {
		return nil, status.InvalidEncryptionAlgorithmError
	}

	if !validIdentifier(srcBucketName) || !validIdentifier(dstBucketName) {
		return nil, status.InvalidObjectName
	}
	if !validIdentifier(srcObjectName) || !validIdentifier(dstObjectName) {
		return nil, status.InvalidObjectName
	}

	srcBucketMeta, ok := meta.GetBucketInfo(srcBucketName)
	if !ok {
		return nil, status.NoSuchBucket
	}

	dstBucketMeta, ok := meta.GetBucketInfo(dstBucketName)
	if !ok {
		return nil, status.NoSuchBucket
	}

	if srcBucketMeta.Location != dstBucketMeta.Location {
		return nil, status.AccessDenied
	}

	srcObjectMeta, ok := meta.GetObjectInfo(srcBucketName, srcObjectName)
	if !ok {
		return nil, status.NoSuchKey
	}

	if !checkObjectAclByBucketMeta(srcBucketMeta.Permission, srcObjectMeta.Permission, srcObjectMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeRead) {
		return nil, status.AccessDenied
	}

	if rc := checkPreHeaders(srcIfModifiedSince, srcIfUnmodifiedSince, srcIfMatch, srcIfNoneMatch, srcObjectMeta); rc != status.Success {
		return nil, rc
	}
	if srcObjectMeta.Type == conf.ObjectTypeAppendable {
		return nil, status.InvalidArgument
	}
	if srcObjectMeta.Size > conf.Config.Limit.MaxCopyObjectSize {
		return nil, status.EntityTooLarge
	}

	dstObjectMeta, ok := meta.GetObjectInfo(dstBucketName, dstObjectName)
	if ok && !checkObjectAclByBucketMeta(dstBucketMeta.Permission, dstObjectMeta.Permission, dstObjectMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
		return nil, status.AccessDenied
	}

	if !ok && !checkBucketACL(dstBucketMeta.Permission, dstBucketMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
		return nil, status.AccessDenied
	}

	var copyObjectResult proto.CopyObjectResult
	if srcBucketName == dstBucketName && srcObjectName == dstObjectName {
		copyObjectResult.ETag = srcObjectMeta.ETag
		copyObjectResult.LastModified = srcObjectMeta.LastModified
		meta.SetObjectInfo(dstBucketName, dstObjectName, srcObjectMeta.OwnerAccessKeyId, srcObjectMeta.ETag, srcObjectMeta.FileLocation, srcObjectMeta.Permission, encryption,
			srcObjectMeta.Type, srcObjectMeta.SymlinkTarget, srcObjectMeta.UserMeta, srcObjectMeta.Crc64Checksum, srcObjectMeta.LastModified, srcObjectMeta.Size)
		return &copyObjectResult, status.Success
	}

	srcLocation := fs.GetObjectLocation(srcBucketName, srcObjectName, 0)
	dstLocation := fs.GetObjectLocation(dstBucketName, dstObjectName, 0)
	tag := srcObjectMeta.ETag
	crcChecksum := srcObjectMeta.Crc64Checksum
	if srcObjectMeta.Type != conf.ObjectTypeSymlink {
		rc := fs.CopyFile(dstLocation, srcLocation, encryption, srcObjectMeta.Encryption, srcObjectMeta.Size)
		if rc != status.Success {
			return nil, rc
		}

		tagChan := fs.CalculateMD5Async(dstLocation, encryption)
		crc64Chan := fs.CalculateCRC64Async(dstLocation)

		tag = <-tagChan
		crcChecksum = <-crc64Chan
		if len(tag) == 0 || len(crcChecksum) == 0 {
			return nil, status.InternalError
		}
	}

	if metaDirective == conf.OssMetadataDirectiveCopy {
		copyObjectResult.ETag = tag
		copyObjectResult.LastModified = srcObjectMeta.LastModified
		newUserMeta := replaceEncryptionUserMeta(srcObjectMeta.UserMeta, encryption)
		meta.SetObjectInfo(dstBucketName, dstObjectName, srcObjectMeta.OwnerAccessKeyId, tag, dstLocation, srcObjectMeta.Permission, encryption,
			srcObjectMeta.Type, srcObjectMeta.SymlinkTarget, newUserMeta, crcChecksum, copyObjectResult.LastModified, srcObjectMeta.Size)
	}
	if metaDirective == conf.OssMetadataDirectiveReplace {
		copyObjectResult.ETag = tag
		copyObjectResult.LastModified = time.Now().Format(conf.OssTimeFormat)
		meta.SetObjectInfo(dstBucketName, dstObjectName, accessKeyId, tag, dstLocation, objectACL, encryption,
			srcObjectMeta.Type, srcObjectMeta.SymlinkTarget, userMeta, crcChecksum, copyObjectResult.LastModified, srcObjectMeta.Size)
	}
	return &copyObjectResult, status.Success
}

func GetObject(requestId string, accessKeyId string, bucketName string, objectName string,
	ifModifiedSince, ifUnmodifiedSince string, ifMatch, ifNoneMatch string) (*meta.Object, status.Status) {
	objectInfo, ok := meta.GetObjectInfo(bucketName, objectName)
	if !ok {
		return nil, status.NoSuchKey
	}

	if !checkObjectAclByBucketName(objectInfo.BucketName, objectInfo.Permission, objectInfo.OwnerAccessKeyId, accessKeyId, conf.AccessTypeRead) {
		return nil, status.AccessDenied
	}

	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}

	if objectInfo.Type != conf.ObjectTypeSymlink {
		if rc := checkPreHeaders(ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, objectInfo); rc != status.Success {
			return nil, rc
		}
	} else {
		targetObjectInfo, ok := meta.GetObjectInfo(bucketName, objectInfo.SymlinkTarget)
		if !ok {
			return nil, status.SymlinkTargetNotExist
		}
		if !checkObjectAclByBucketName(targetObjectInfo.BucketName, targetObjectInfo.Permission, targetObjectInfo.OwnerAccessKeyId, accessKeyId, conf.AccessTypeRead) {
			return nil, status.AccessDenied
		}
		if targetObjectInfo.Type == conf.ObjectTypeSymlink {
			return nil, status.InvalidTargetType
		}
		if rc := checkPreHeaders(ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, targetObjectInfo); rc != status.Success {
			return nil, rc
		}
		return targetObjectInfo, status.Success
	}
	return objectInfo, status.Success
}

func HeadObject(requestId, accessKeyId, bucketName, objectName, ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch string) (map[string]string, status.Status) {
	objectInfo, ok := meta.GetObjectInfo(bucketName, objectName)
	if !ok {
		return nil, status.NoSuchKey
	}

	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}

	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}

	if !checkObjectAclByBucketName(objectInfo.BucketName, objectInfo.Permission, objectInfo.OwnerAccessKeyId, accessKeyId, conf.AccessTypeRead) {
		return nil, status.AccessDenied
	}

	if rc := checkPreHeaders(ifModifiedSince, ifUnmodifiedSince, ifMatch, ifNoneMatch, objectInfo); rc != status.Success {
		return nil, rc
	}

	if objectInfo.Type == conf.ObjectTypeSymlink {
		objectTargetMeta, ok := meta.GetObjectInfo(bucketName, objectInfo.SymlinkTarget)
		if !ok {
			return nil, status.SymlinkTargetNotExist
		}
		if objectTargetMeta.Type == conf.ObjectTypeSymlink {
			return nil, status.InvalidTargetType
		}
		return getHeaders(objectInfo, objectTargetMeta)
	}
	return getHeaders(objectInfo, nil)
}

func AppendObject(requestId, accessKeyId, bucketName, objectName, contentDispositon, contentChecksum, encryption, permission,
	positionStr, userMeta string, contentLength int64, document io.ReadCloser) (*meta.Object, status.Status) {
	if contentLength < 0 {
		return nil, status.MissingContentLength
	}
	if contentLength > conf.Config.Limit.MaxFileSize {
		return nil, status.InvalidArgument
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}
	if len(encryption) != 0 && encryption != "AES256" {
		return nil, status.InvalidEncryptionAlgorithmError
	}
	if len(positionStr) == 0 {
		return nil, status.InvalidArgument
	}
	position, err := strconv.ParseInt(positionStr, 10, 64)
	if err != nil {
		return nil, status.InternalError
	}

	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}

	objectMeta, ok := meta.GetObjectInfo(bucketName, objectName)
	var fileLocation string
	if ok {
		if !checkObjectAclByBucketName(objectMeta.BucketName, objectMeta.Permission, objectMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
			return nil, status.AccessDenied
		}
		if objectMeta.Type != conf.ObjectTypeAppendable {
			return nil, status.ObjectNotAppendable
		}
		if position != objectMeta.Size {
			return nil, status.PositionNotEqualToLength
		}
		if contentLength+objectMeta.Size > conf.Config.Limit.MaxFileSize {
			return nil, status.InvalidArgument
		}
		if encryption != objectMeta.Encryption {
			return nil, status.InvalidArgument
		}
		if contentLength == 0 {
			return nil, status.Success
		}

		fileLocation = objectMeta.FileLocation
		encryption = objectMeta.Encryption
		rc := fs.AppendFile(fileLocation, document, objectMeta.Size, contentLength, encryption)
		if rc != status.Success {
			return nil, rc
		}
		contentLength = contentLength + objectMeta.Size
	} else {
		if position != 0 {
			return nil, status.InvalidArgument
		}
		bucketMeta, ok := meta.GetBucketInfo(bucketName)
		if !ok {
			return nil, status.NoSuchBucket
		}
		if !checkBucketACL(bucketMeta.Permission, bucketMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
			return nil, status.AccessDenied
		}
		fileLocation = fs.GetObjectLocation(bucketName, objectName, 0)
		rc := fs.WriteFile(fileLocation, document, contentLength, encryption)
		if rc != status.Success {
			return nil, rc
		}
	}

	tag := fs.GenerateContentUUID(fileLocation, encryption, conf.Config.Security.AesPrivateKey)
	crcChecksum := fs.CalculateCRC64(fileLocation)

	if len(tag) == 0 || len(crcChecksum) == 0 {
		return nil, status.InternalError
	}
	appendedObjectMeta := meta.SetObjectInfo(bucketName, objectName, accessKeyId, tag, fileLocation, permission, encryption,
		conf.ObjectTypeAppendable, "", userMeta, crcChecksum, time.Now().Format(conf.OssTimeFormat), contentLength)

	return appendedObjectMeta, status.Success
}

func DeleteObject(requestId, accessKeyId, bucketName, objectName string) status.Status {
	if !validIdentifier(bucketName) {
		return status.InvalidBucketName
	}

	if !validIdentifier(objectName) {
		return status.InvalidObjectName
	}

	objectMeta, ok := meta.GetObjectInfo(bucketName, objectName)
	if !ok {
		return status.NoSuchKey
	}

	if !checkObjectAclByBucketName(objectMeta.BucketName, objectMeta.Permission, objectMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
		return status.AccessDenied
	}

	if !meta.DeleteObject(bucketName, objectName) {
		return status.InternalError
	}

	if objectMeta.Type != conf.ObjectTypeSymlink {
		objectLocation := fs.GetObjectLocation(bucketName, objectName, 0)
		err := removeAll(objectLocation)
		if err != nil && !os.IsNotExist(err) {
			return status.InternalError
		}
	}

	return status.Success
}

func GetObjectMeta(requestId, accessKeyId, bucketName, objectName string) (headers map[string]string, rc status.Status) {
	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}
	objectMeta, ok := meta.GetObjectInfo(bucketName, objectName)
	if !ok {
		return nil, status.NoSuchKey
	}
	if !checkObjectAclByBucketName(objectMeta.BucketName, objectMeta.Permission, objectMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeRead) {
		return nil, status.AccessDenied
	}

	headers = make(map[string]string)
	headers[conf.HTTPHeaderEtag] = objectMeta.ETag
	headers[conf.HTTPHeaderContentLength] = strconv.FormatInt(objectMeta.Size, 10)
	headers[conf.HTTPHeaderLastModified] = objectMeta.LastModified

	return headers, status.Success
}

func GetObjectAcl(requestId, accessKeyId, bucketName, objectName string) (*proto.AccessControlPolicy, status.Status) {
	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}
	bucketMeta, ok := meta.GetBucketInfo(bucketName)
	if !ok {
		return nil, status.NoSuchBucket
	}
	if bucketMeta.OwnerAccessKeyId != accessKeyId {
		return nil, status.AccessDenied
	}

	objectMeta, ok := meta.GetObjectInfo(bucketName, objectName)
	if !ok {
		return nil, status.NoSuchKey
	}
	var accessControlPolicy proto.AccessControlPolicy
	if objectMeta.Permission != "" {
		accessControlPolicy.AccessControlList.Grant = objectMeta.Permission
	} else {
		accessControlPolicy.AccessControlList.Grant = conf.PermissionObjectDefault
	}
	authInfo, _ := meta.GetAuthInfo(accessKeyId)
	accessControlPolicy.Owner.ID, accessControlPolicy.Owner.DisplayName = authInfo.AuthID, authInfo.DisplayName
	return &accessControlPolicy, status.Success
}

func PutObjectAcl(requestId, accessKeyId, bucketName, objectName, permission string) status.Status {
	if !validIdentifier(bucketName) {
		return status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return status.InvalidObjectName
	}

	if !validPermission(permission) {
		return status.InvalidArgument
	}

	_, ok := meta.GetBucketInfo(bucketName)
	if !ok {
		return status.NoSuchBucket
	}

	objectMeta, ok := meta.GetObjectInfo(bucketName, objectName)
	if !ok {
		return status.NoSuchKey
	}

	if objectMeta.OwnerAccessKeyId != accessKeyId {
		return status.AccessDenied
	}

	if !meta.SetObjectPermission(objectMeta, permission) {
		return status.InternalError
	}

	return status.Success
}

func DeleteMultipleObjects(requestId, accessKeyId, bucketName string, wantDelete *proto.Delete) (deleteResult *proto.DeleteResult, rc status.Status) {
	if len(wantDelete.Object) > conf.MaxDeleteMultiObjects {
		return nil, status.MalformedXML
	}
	var deleteResultVerbose proto.DeleteResult
	var deleteResultQuiet proto.DeleteResult
	for _, v := range wantDelete.Object {
		rc := DeleteObject(requestId, accessKeyId, bucketName, v.Key)
		if rc == status.Success {
			deleteResultVerbose.Deleted = append(deleteResultVerbose.Deleted, proto.Deleted{Key: v.Key})
		} else {
			deleteResultQuiet.Error = append(deleteResultQuiet.Error,
				proto.DeletedError{
					Key:     v.Key,
					Code:    status.GetCodeString(rc),
					Message: status.GetMessage(rc),
			})

			deleteResultVerbose.Error = append(deleteResultVerbose.Error,
				proto.DeletedError{
					Key:     v.Key,
					Code:    status.GetCodeString(rc),
					Message: status.GetMessage(rc),
				})
		}
	}

	if wantDelete.Quiet == strconv.FormatBool(false) || wantDelete.Quiet == "" {
		deleteResult = &deleteResultVerbose
	} else {
		deleteResult = &deleteResultQuiet
	}
	return deleteResult, rc
}

func PostObject(requestId, bucketName, objectName, contentChecksum string, fileSizePost int64,
	formData url.Values, fileUpload multipart.File) (headers map[string]string, rc status.Status) {

	accessKeyId := formData.Get(conf.OssFormFieldOSSAccessKeyId)
	permission := formData.Get(conf.HTTPHeaderOssObjectACL)
	encryption := formData.Get(conf.OssFormFieldOssServerSideEncryption)
	policy := formData.Get(conf.OssFormFieldPolicy)
	signature := formData.Get(conf.OssFormFieldSignature)

	headers = make(map[string]string)
	if encryption != "" && encryption != "AES256" {
		return nil, status.InvalidEncryptionAlgorithmError
	}
	headers[conf.HTTPHeaderOssServerSideEncryption] = encryption
	if fileSizePost > conf.Config.Limit.MaxFileSize {
		return nil, status.EntityTooLarge
	}

	if formData.Get(conf.OssFormFieldKey) == "" {
		return nil, status.InvalidArgument
	}

	if !(accessKeyId == "" && policy == "" && signature == "") && !(accessKeyId != "" && policy != "" && signature != "") {
		return nil, status.InvalidArgument
	}

	var userMetas []string
	for k := range formData {
		if strings.HasPrefix(k, conf.OssFormFieldOssMetaPrefix) {
			userMetas = append(userMetas, k)
			userMetas = append(userMetas, formData.Get(k))
		}
	}
	userMeta := strings.Join(userMetas, conf.UserMeaSep)
	if getSizeOfUserMeta(userMeta) > conf.UserMetaMaxSize {
		return nil, status.InvalidArgument
	}

	bucketMeta, ok := meta.GetBucketInfo(bucketName)
	if !ok {
		return nil, status.NoSuchBucket
	}
	if !checkBucketACL(bucketMeta.Permission, bucketMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
		return nil, status.AccessDenied
	}

	fileLocation := fs.GetObjectLocation(bucketName, objectName, 0)
	rc = fs.WriteFile(fileLocation, fileUpload, fileSizePost, encryption)
	if rc != status.Success {
		return nil, rc
	}

	tag := fs.GenerateContentUUID(fileLocation, encryption, conf.Config.Security.AesPrivateKey)
	crcChecksum := fs.CalculateCRC64(fileLocation)

	if len(tag) == 0 || len(crcChecksum) == 0 {
		return nil, status.InternalError
	}

	meta.SetObjectInfo(bucketName, objectName, accessKeyId, tag, fileLocation, permission, encryption,
		conf.ObjectTypeNormal, "", userMeta, crcChecksum, time.Now().Format(conf.OssTimeFormat), fileSizePost)

	return headers, status.Success
}

func replaceEncryptionUserMeta(userMeta string, encryption string) string {
	tokens := strings.Split(userMeta, ",")
	found := false
	for i := 0; i < len(tokens); i += 2 {
		if tokens[i] == conf.HTTPHeaderOssServerSideEncryption {
			tokens[i+1] = encryption
			found = true
			break
		}
	}
	if found {
		userMeta = strings.Join(tokens, ",")
	} else if len(encryption) != 0 {
		userMeta = userMeta + "," + conf.HTTPHeaderOssServerSideEncryption + "," + encryption
	}
	return userMeta
}
